<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 fdm=marker: */

/**
 * LightBot_Protocol_Jabber_ProtocolAdapter
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category  LightBot
 * @package   LightBot_Protocol_Jabber
 * @copyright Copyright (c) 2008 Olivier Hoareau <hoareau.olivier@gmail.com>
 * @author    Olivier Hoareau <hoareau.olivier@gmail.com>
 * @license   License http://framework.zend.com/license   New BSD License
 * @version   $Id: ProtocolAdapter.php 76 2008-06-13 11:22:03Z hoareau.olivier $
 */

require_once dirname(__FILE__) . '/../../IProtocolAdapter.php';

// {{{ LightBot_Protocol_Jabber_ProtocolAdapter

/**
 * Jabber Protocol Adapter
 *
 * @category  LightBot
 * @package   LightBot_Protocol_Jabber
 * @copyright Copyright (c) 2008 Olivier Hoareau <hoareau.olivier@gmail.com>
 * @author    Olivier Hoareau <hoareau.olivier@gmail.com>
 * @license   License http://framework.zend.com/license   New BSD License
 */
class LightBot_Protocol_Jabber_ProtocolAdapter implements LightBot_IProtocolAdapter
{
    // {{{ properties

    /**
     * Configuration properties
     * @var array
     */
    private $conf;

    /**
     * The lightbot
     * @var LightBot_ILightBot
     */
    private $lightBot;

    /**
     * The underlying socket adapter
     * @var LightBot_ISocketAdapter
     */
    private $socketAdapter;

    /**
     * The opened socket
     * @var resource
     */
    private $socket;

    /**
     * Last info
     * @var array
     */
    private $last;

    /**
     * Received raw data queue
     * @var array
     */
    private $received;

    /**
     * Queued actions
     * @var array
     */
    private $actions;

    /**
     * Remote Actions
     * @var array
     */
    private $remoteActions;

    // }}}
    // {{{ __construct()

    /**
     * Constructs a new LightBot
     */
    public function __construct()
    {
        $this->socketAdapter = null;
        $this->socket        = null;

        set_magic_quotes_runtime(0);

        $this->conf              = null;
        $this->last['keepalive'] = null;
        $this->received          = array();
        $this->actions           = array();
        $this->remoteActions     = array('handles'=>array(),'urls'=>array());
    }

    // }}}
    // {{{ configure()

    /**
     * Configures the protocol adapter with the specified configuration settings
     *
     * @param  mixed     $conf array
     * @return LightBot_IProtocolAdapter  $this
     * @throws Exception if an error occured
     */
    public function configure($conf)
    {
        $this->conf = $conf;
        if(!array_key_exists('login',$this->conf)) throw new Exception("No 'login' key specified in the configuration");
        @list($this->conf['name'],$this->conf['host']) = explode('@',$this->conf['login']);
        if(strlen($this->conf['name'])===0) throw new Exception("Login is empty");
        if(strlen($this->conf['host'])===0) throw new Exception("Login does not contains the host information");
        
        if(!array_key_exists('realhost',$this->conf)) $this->conf['realhost'] = 'talk.google.com';
        if(!array_key_exists('port',$this->conf)) $this->conf['port'] = 5222;
        if(!array_key_exists('client',$this->conf)) $this->conf['client'] = 'Lightbot.v'.md5(file_get_contents(__FILE__));
        if(!array_key_exists('status',$this->conf)) $this->conf['status'] = 'Hello !';
        if(!array_key_exists('show',$this->conf)) $this->conf['show'] = 'chat';
        if(!array_key_exists('keepalive',$this->conf)) $this->conf['keepalive'] = 3;
        return $this;
    }

    // }}}
    // {{{ connect()

    /**
     * Connects the bot to the jabber server specified in the configuration
     *
     * @return boolean   always true
     * @throws Exception if a connection error occured
     */
    public function connect()
    {
        if($this->socketAdapter===null) throw new Exception("No socket adapter defined");

        if($this->socket!==null){
            $this->getLightBot()->getLogger()->warn("Bot is already connected, trying to disconnect before reconnecting...");
            try{
                $this->disconnect();
            }catch(Exception $e){
                $this->getLightBot()->getLogger()->error("Unable to disconnect: ".$e->getMessage());
            }
        }

        $listener = $this->lightBot->getListener();

        // open server socket connection
        $socket = $this->socketAdapter->open($this->conf['realhost'],$this->conf['port']);

        $this->getLightBot()->getLogger()->info("Connected to Jabber server '{$this->conf['host']}:{$this->conf['port']}'");

        // open stream
        $this->socketAdapter->write($socket,"<?xml version='1.0' encoding='UTF-8' ?><stream:stream to='{$this->conf['host']}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>");

        // wait 2 seconds
        sleep(2);

        // read stream
        $raw = $this->socketAdapter->read($socket);

        // check if stream is opened
        if(strpos($raw,'<stream:stream')===false) throw new Exception("No stream opened by the remote server");

        // notifies the listener of the connection
        $listener->connected(array('_type'=>'connect','id'=>null,'from'=>utf8_encode($this->conf['name']),'from_client'=>utf8_encode($this->conf['client'])));

        // check start tls capabilities of jabber server
        if(strpos($raw,'<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls">')===false) throw new Exception("Unable to start tls authentication to jabber server");

        // send start tls directive
        $this->socketAdapter->write($socket,"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");

        // wait 2 seconds
        sleep(2);

        // read start tls acknowledge
        $raw = $this->socketAdapter->read($socket);

        // check start tls acknowledge of jabber server
        if(strpos($raw,'<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>')===false) throw new Exception("No start tls method acknowledge received from jabber server");
        // crypting connection
        $this->socketAdapter->crypt($socket);

        // open jabber stream
        $this->socketAdapter->write($socket,"<?xml version='1.0' encoding='UTF-8' ?><stream:stream to='{$this->conf['host']}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>");

        // wait 2 seconds
        sleep(2);

        // send authentication request
        $this->socketAdapter->write($socket,"<iq type='get' id='auth'><query xmlns='jabber:iq:auth'><username>".utf8_encode($this->conf['name'])."</username></query></iq>");

        // wait 1 second
        sleep(1);

        // read stream
        $raw = $this->socketAdapter->read($socket);

        // check plain authentication capabilities of jabber server
        if(strpos($raw,'<mechanism>PLAIN</mechanism>')===false) throw new Exception("Jabber server does not support plain authentication over crypted connection");

        // check plain authentication acknowledge of jabber server
        if(strpos($raw,'<query xmlns="jabber:iq:auth"><username/><resource/><password/></query>')===false) throw new Exception("Jabber did not acknowledge our plain authentication request");

        // send authentication credentials
        $this->socketAdapter->write($socket,"<iq type='set' id='authcred'><query xmlns='jabber:iq:auth'><username>".utf8_encode($this->conf['name'])."</username><password>".utf8_encode($this->conf['pass'])."</password><resource>".utf8_encode($this->conf['client'])."</resource></query></iq>");

        // wait 2 second
        sleep(2);

        // read status of authentication
        $raw = $this->socketAdapter->read($socket);

        // check status of authentication
        if(strpos($raw,'<iq id="authcred" type="result"/>')===false) throw new Exception("Jabber authentication failed");

        $this->getLightBot()->getLogger()->info("Authenticated on Jabber server '{$this->conf['host']}' with account '{$this->conf['name']}'");

        // notifies the listener of the authentication
        $listener->loggedIn(array('_type'=>'login','id'=>null,'from'=>utf8_encode($this->conf['name']).'@'.$this->conf['host'],'from_client'=>utf8_encode($this->conf['client'])));
        
        // sends update roster request to be able to receive subscription event (do that before sending first presence !)
        $this->socketAdapter->write($socket,"<iq type='get' id='get_roster' ><query xmlns='jabber:iq:roster'/></iq>");

        // send presence information
        $this->socketAdapter->write($socket,"<presence><status>".utf8_encode($this->conf['status'])."</status><show>".utf8_encode($this->conf['show'])."</show></presence>");

        // connection is opened and fully operational
        $this->socket = $socket;
        return true;
    }

    // }}}
    // {{{ disconnect()

    /**
     * Disconnects the bot from the server
     *
     * @return boolean always true
     * @throws Exception if socket is null or closed, no socket adapter defined
     */
    public function disconnect()
    {
        if($this->socketAdapter===null) throw new Exception("No socket adapter defined");

        if($this->socket===null) throw new Exception("Bot is not connected");

        $this->socketAdapter->close($this->socket);

        $listener = $this->lightBot->getListener();

        // notifies the listener of the log out
        $listener->loggedOut(array('_type'=>'logout','id'=>null,'from'=>utf8_encode($this->conf['name']).'@'.$this->conf['host'],'from_client'=>utf8_encode($this->conf['client'])));

        // notifies the listener of the disconnection
        $listener->disconnected(array('_type'=>'disconnect','id'=>null,'from'=>utf8_encode($this->conf['name']),'from_client'=>utf8_encode($this->conf['client'])));

        $this->getLightBot()->getLogger()->info("Disconnected from Jabber server '{$this->conf['host']}:{$this->conf['port']}' for account '{$this->conf['name']}'");
        return true;
    }

    // }}}
    // {{{ receive()

    public function receive()
    {
        try {
            $received = $this->socketAdapter->read($this->socket);
            // pushes the received raw data to the received events queue
            if(strlen($received)) $this->received[] = $received;
        } catch(Exception $e) {
            if(!$this->socketAdapter->isSocketOpened($this->socket)) {
                try {
                    $this->connect();
                } catch(Exception $e2) {
                    throw new Exception("Connection lost: ".$e->getMessage());
                }
                return $this->socketAdapter->read($this->socket);
            } else {
                throw new Exception("Socket is opened but a read error occured: ".$e->getMessage());
            }
        }
    }

    // }}}
    // {{{ processIncoming()

    /**
     * Processes the received events in the queue
     *
     * @param LightBot_IListener $listener the listener to notify of known received events
     * @return LightBot_IProtocolAdapter $this
     */
    public function processIncoming()
    {
        $listener = $this->lightBot->getListener();
        if($listener===null) return;

        $notcomplete=false;
        while(count($this->received)>0 && $notcomplete===false){
            $raw = array_shift($this->received);
            // if incoming events received
            $trimed = trim($raw);
            if ($trimed!=='') {
                    if(substr($trimed,strlen($trimed)-1)!=='>'){
                        // we probably do not receive all the data
                        // unless there is a more recent packet received
                        if(count($this->received)>0){
                            // we try to merge it with the more recent packet
                            $this->received[count($this->received)-1] = $raw . $this->received[count($this->received)-1];
                            $notcomplete=true;
                        }else{
                            $this->received[] = $raw;
                            $notcomplete=true;
                        }
                        break;
                    }elseif(substr($trimed,0,1)!=='<' && count($this->received)>0){
                            // we try to merge it with the more older packet
                            $this->received[count($this->received)-1] .=$raw;
                            $notcomplete=true;
                    }

                    // splits the incoming raw data into events
                    $temp = preg_split("/<(message|iq|presence|stream:stream|proceed|\\/stream:stream)/", $raw, -1, PREG_SPLIT_DELIM_CAPTURE);
                    // process all incoming events
                    for ($a = 1; $a < count($temp); $a = $a + 2) {

                        // send keep alive
                        $this->ping();

                        $data = "<" . $temp[$a] . $temp[($a + 1)];
                
                        // process one event
                        if(strpos($data,'<stream:stream ')!==false && strpos($data,'</stream:stream>')===false) {
                            $data.='</stream:stream>';
                        }elseif(strpos($data,'</stream:stream>')!==false) {
                            $data='<stream:stream>'.$data;
                        }

                        $x = @new SimpleXMLElement($data);
                        $xa = $x->attributes();

                        $evts = array();

                        switch($temp[$a]) {
                        case 'message':
                            @list($from,$from_client)=explode('/',(string)$xa['from']);
                            @list($to,$to_client)=explode('/',(string)$xa['to']);
                            if(@$x->x->composing){
                                $listener->composingReceived(array('_type'=>'composing','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client,'to'=>$to,'to_client'=>$to_client,'composing'=>true));
                            }elseif(@$x->x && array_shift($x->x->getDocNamespaces())==='jabber:x:event'){
                                $listener->composingReceived(array('_type'=>'composing','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client,'to'=>$to,'to_client'=>$to_client,'composing'=>false));
                            }
                            if((string)@$xa['type']==='error') {
                                // @todo
                            }elseif(@$x->body) {           
                                $listener->messageReceived(array('_type'=>'message','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client,'to'=>$to,'to_client'=>$to_client,'body'=>utf8_decode((string)$x->body),'type'=>(string)@$xa['type']));
                            }elseif(@$x->html) {
                                $listener->messageReceived(array('_type'=>'message','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client,'to'=>$to,'to_client'=>$to_client,'body'=>strip_tags(utf8_decode((string)$x->html)),'type'=>(string)@$xa['type']));
                            }
                            break;
                        case 'iq':
                            switch((string)@$xa['type']){
                            case 'error':
                                @list($from,$from_client)=explode('/',(string)$xa['from']);
                                @list($to,$to_client)=explode('/',(string)$xa['to']);
                                $xae = (@$x->error[0] instanceof SimpleXMLElement ? $x->error->attributes() : array());
                                $listener->errorReceived(array('_type'=>'error','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client,'to'=>$to,'to_client'=>$to_client,'code'=>@$xae['code'],'type'=>@$xae['type'],'message'=>(@$x->error[0] instanceof SimpleXMLElement) ? @$x->error[0]->getName() : null));
                                break;
                            case 'set':
                                break;
                            case 'get':
                                break;
                            case 'result':
                                @list($to,$to_client)=explode('/',(string)$xa['to']);
                                if(@$x->vCard){
                                    // vcard info
                                    @list($from,$from_client)=explode('/',(string)$xa['from']);
                                    $listener->vCardUpdated(array('_type'=>'vcard','to'=>$to,'to_client'=>$to_client,'from'=>$from,'from_client'=>$from_client,'vcard'=>@$x->vCard->asXML()));
                                }else{
                                    // roster
                                    $contacts = array();
                                    if(@$x->query && @$x->query[0]->item) {
                                        foreach($x->query[0]->item as $item) {
                                            $groups = array();
                                            $xai = $item->attributes();
                                            if(@is_array($item->group)) {
                                                foreach($item->group as $group) {
                                                    $groups[] = (string)$group;
                                                }
                                            }
                                            $contacts[] = array('jid'=>(string)@$xai['jid'],'name'=>utf8_decode((string)@$xai['name']),'subscription'=>(string)@$xai['subscription']);
                                        }
                                    }
                                    $listener->contactsUpdated(array('_type'=>'contacts','to'=>$to,'to_client'=>$to_client,'contacts'=>$contacts));
                                }
                                break;
                            default:
                                break;
                            }
                            break;
                        case 'presence':
                            @list($from,$from_client)=explode('/',(string)$xa['from']);
                            @list($to,$to_client)=explode('/',(string)$xa['to']);
                            switch((string)$xa['type']) {
                            case 'subscribe':
                                $listener->subscriptionRequestReceived(array('_type'=>'subscribe','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client,'to'=>$to,'to_client'=>$to_client));
                                break;
                            case 'subscribed':
                                $listener->subscriptionConfirmationReceived(array('_type'=>'subscribed','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client,'to'=>$to,'to_client'=>$to_client));
                                break;
                            case 'usubscribe':
                                $listener->unsubscriptionRequestReceived(array('_type'=>'unsubscribe','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client,'to'=>$to,'to_client'=>$to_client));
                                break;
                            case 'unsubscribed':
                                $listener->unsubscriptionConfirmationReceived(array('_type'=>'subscribed','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client,'to'=>$to,'to_client'=>$to_client));
                                break;
                            default:
                                $photo_version = '';
                                if(@is_array($x->x)){
                                    foreach($x->x as $x2){
                                        if(@$x2->photo) {$photo_version = (string)$x2->photo;break;}
                                    }
                                }
                                $listener->presenceReceived(array('_type'=>'presence','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client,'to'=>$to,'to_client'=>$to_client,'status'=>(string)@$x->status,'show'=>(string)@$x->show,'priority'=>(string)@$x->priority,'type'=>(string)@$xa['type'],'photo_version'=>$photo_version));
                                break;
                            }
                            break;
                            /*
                        case 'stream:stream':
                            @list($from,$from_client)=explode('/',(string)$xa['from']);
                            $listener->connected(array('_type'=>'connect','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client));
                            break;
                        case 'proceed':
                            break;
                        case '/stream:stream':
                            @list($from,$from_client)=explode('/',(string)$xa['from']);
                            $listener->loggedOut(array('_type'=>'login','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client));
                            $listener->disconnected(array('_type'=>'disconnect','id'=>(string)@$xa['id'],'from'=>$from,'from_client'=>$from_client));
                            break;
                             */
                        default:
                            break;
                        }
                    }
                }
            }
    }

    // }}}
    // {{{ processOutgoing()

    /**
     * Processes the listed outgoing actions
     *
     * @return LightBot_IProtocolAdapter the protocol adapter
     */
    public function processOutgoing()
    {
        // queued actions
        $nb = count($this->actions);
        $nbloop=0;
        while(count($this->actions)>0 && $nbloop<=$nb){
            $raw = array_shift($this->actions);
            // are we trying to send a message ?
            if(substr($raw,0,8)==='<message'){
                // yes, event to send is a message
                if($this->lastMessagesTime===null) $this->lastMessagesTime = time();
                if((time()-$this->lastMessagesTime)<2){
                    // we send a message less than 2 seconds ago
                    if($this->lastMessagesCount>=10){
                        // we send more than 10 messages the last 2 seconds
                        $this->actions[] = $raw;
                        $nbloop++;
                        continue;
                    }else{
                        $this->lastMessagesCount++;
                    }
                }else{
                    $this->lastMessagesCount=1;
                    $this->lastMessagesTime=time();
                }
            }
            $this->socketAdapter->write($this->socket,$raw);
        }

        // queued remote actions
        $read = $this->remoteActions['handles'];
        if(count($read)>0){
            while(count($this->remoteActions['handles'])>0 && @stream_select($read,$write=null,$except=null,0)>0){
                foreach($read as $fp){
                    $t = '';
                    while(!feof($fp)){
                        $t.=fread($fp,1024);
                    }
                    if($t){
                        @list($headers,$tt) = explode("\r\n\r\n",$t);
                        @list($prot,$code) = explode(' ',substr($headers,0,12));
                        switch($code){
                        case 200:
                            // no problem
                            break;
                        case 403:
                        case 404:
                        default:
                            throw new Exception("HTTP $code received");
                        }
                        $actions = $this->parsePostActions(trim($tt));
                        foreach($actions as $action){
                            $event = $action['properties'];
                            $event['_type'] = $action['type'];
                            switch(@$event['_type']){
                            case 'message': $this->pushMessage($event); break;
                            case 'composing': $this->pushComposing($event); break;
                            case 'add-contact': $this->pushAddContact($event); break;
                            case 'remove-contact': $this->pushRemoveContact($event); break;
                            case 'presence': $this->pushPresence($event); break;
                            case 'subscribe': $this->pushSubscriptionRequest($event); break;
                            case 'subscribed': $this->pushSubscriptionConfirmation($event); break;
                            case 'unsubscribe': $this->pushUnsubscriptionRequest($event); break;
                            case 'unsubscribed': $this->pushUnsubscriptionConfirmation($event); break;
                            case 'presence': $this->pushPresence($event); break;
                            case 'error': $this->pushError($event); break;
                            case 'mail':
                                if(!@$event['from']){
                                    $event['from'] = $this->conf['login'];
                                }
                                try{
                                    $this->getLightBot()->getActionAdapter()->sendMail($event);
                                }catch(Exception $e){
                                    $this->getLightBot()->getLogger()->error("Unable to send email ".json_encode($event).": ".$e->getMessage());
                                }
                                break;
                            case 'update-contacts': $this->pushListContactRequest($event); break;
                            case 'update-vcard': $this->pushContactVCardRequest($event); break;
                            default:
                                break;
                            }
                        }
                    }
                    stream_socket_shutdown($fp, STREAM_SHUT_RDWR);
                    fclose($fp);
                }
            }
        }

        // cleans the closed streams
        $toCleanActions = $this->remoteActions['handles'];
        foreach(array_keys($toCleanActions) as $i){
            if(!is_resource($toCleanActions[$i]) || feof($toCleanActions[$i])) {unset($this->remoteActions['handles'][$i]);unset($this->remoteActions['urls'][$i]);}
        }

        return $this;
    }

    // }}}
    // {{{ parsePostActions()

    /**
     * Parses the post actions received from the remote action execution
     *
     * @param string $raw the raw actions to parse
     * @return array the list of parsed post actions
     */
    protected function parsePostActions($raw)
    {
        $decoded = json_decode($raw,true);
        if(!is_array($decoded)) {return array();}
        return array_key_exists('actions',$decoded) ? $decoded['actions'] : array();
    }

    // }}}
    // {{{ pushRaw()

    /**
     * Pushes the specified raw event to the list of events to send in next send loop
     *
     * @param  string             $raw the raw event/data to send
     * @return LightBot_IProtocolAdapter $this
     */
    protected function pushRaw($raw)
    {
        $this->actions[] = $raw;
    }

    // }}}
    // {{{ setLightBot()

    /**
     * Sets the attached light bot
     *
     * @param LightBot_ILightBot $lightBot the bot
     * @return LightBot_IProtocolAdapter $this
     */
    public function setLightBot(LightBot_ILightBot $lightBot)
    {
        $this->lightBot = $lightBot;
        return $this;
    }

    // }}}
    // {{{ getLightBot()

    /**
     * Returns the attached light bot
     *
     * @return LightBot_ILightBot the bot
     */
    public function getLightBot()
    {
        return $this->lightBot;
    }

    // }}}
    // {{{ setSocketAdapter()

    /**
     * Sets the underlying socket adapter
     * 
     * @param  LightBot_ISocketAdapter $socketAdapter the socket adpater
     * @return LightBot                $this
     */
    public function setSocketAdapter(LightBot_ISocketAdapter $socketAdapter)
    {
        $this->socketAdapter = $socketAdapter;
        return $this;
    }

    // }}}
    // {{{ getSocketAdapter()

    /**
     * Returns the underlying socket adapter
     *
     * @return LightBot_ISocketAdapter the underlying socket adapter
     */
    public function getSocketAdapter()
    {
        return $this->socketAdapter;
    }

    // }}}
    // {{{ filterCommonOutgoingParameters()

    /**
     * Checks/Filters the common parameters of the outgoing message
     *
     * @param  array             $msg the outgoing message
     * @param  array             $mandatories the mandatories parameters to check
     * @return array             the outgoing message filtered
     * @throws Exception         if at least one of the common parameters has errors (missing, bad format...)
     */
    protected function filterCommonOutgoingParameters($msg,$mandatories=array())
    {
        // id
        if (!@$msg['id'])   $msg['id']   = 'auto_'.@$msg['_type'] . "_" . md5(time().microtime());

        // from
        if (!@$msg['from'] && in_array('from',$mandatories)===true) $msg['from'] = $this->conf['name'] . '@' . $this->conf['host'];

        // to
        //if(!@$msg['to'])    throw new Exception("Missing recipient in outgoing jabber event (attribute 'to')");
        if(@$msg['to']===$this->conf['host']) throw new Exception("Could not send jabber event: recipient is jabber server: ".$msg['to']);
        if(@$msg['to']===($this->conf['name'].'@'.$this->conf['host']) || (@$msg['to']!==null && @$msg['to']===$this->conf['name']))
        {
            throw new Exception("Could not send jabber event: recipient is same as sender: ".$msg['to']);
        }

        foreach($mandatories as $m) {
            if(!@$msg[$m]) throw new Exception("Missing parameter '$m' in outgoing jabber event");
        }

        return $msg;
    }

    // }}}
    // {{{ ping()

    /**
     * Pings the remote server if needed
     *
     * @return LightBot  $this
     * @throws Exception if socket is null or closed
     */
    public function ping()
    {
        if ($this->last['keepalive']===null && (time()-$this->last['keepalive'])>$this->conf['keepalive']) {
            $this->socketAdapter->write($this->socket,"<iq type='get' from='".utf8_encode($this->conf['name'].'@'.$this->conf['host'].'/'.$this->conf['client'])."' to='{$this->conf['host']}' id='keep_alive'><query xmlns='jabber:iq:time' /></iq>");
            $this->last['keepalive'] = time();
        }
    }

    // }}}
    // {{{ pushMessage()

    /**
     * Pushes the specified message to the list of messages to send
     *
     * @param   array              $msg the message
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushMessage($msg)
    {
        $msg = $this->filterCommonOutgoingParameters($msg,array('to'));

        $type    = @$msg['type'] ? $msg['type'] : 'chat';
        $subject = htmlspecialchars(@$msg['subject']);
        $thread  = htmlspecialchars(@$msg['thread']);
        $body    = htmlspecialchars(@$msg['body']);
        $bodyXhtml = htmlspecialchars(@$msg['bodyxhtml']);

        // compose event inside ?
        if(@$msg['cid']){
            $this->pushComposing($msg);
        }
        $msgBegin = "<message to='";
        $msgEnd   = "' type='".$type."' id='".$msg['id']."'>".($subject?("<subject>".$subject."</subject>"):"").($thread?("<thread>".$thread."</thread>"):"").($body?"<body>$body</body>":"").($bodyxhtml?"<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'>$bodyxhtml</body></html>":"").@$msg['payload']."</message>\n";
        foreach(explode(',',$msg['to']) as $to){
            $this->pushRaw($msgBegin.$to.$msgEnd);
        }

        return $this;
    }

    // }}}
    // {{{ pushComposing()

    /**
     * Pushes the specified compose event to the list of messages to send
     *
     * @param   array              $event the event
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushComposing($event)
    {
        $event = $this->filterCommonOutgoingParameters($event,array('from','to'));

        $id = @$event['cid'];
        $stop = @$event['stop']===true;
        
        $this->pushRaw("<message to='".$event['to']."'><x xmlns='jabber:x:event'>".($id?"<id>$id</id>":"").(!$stop?"<composing/>":"")."</x></message>\n");
        return $this;
    }

    // }}}
    // {{{ pushSubscriptionConfirmation()

    /**
     * Pushes the specified subscription confirmation message to the list of messages to send
     *
     * @param   array              $msg the message
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushSubscriptionConfirmation($msg)
    {
        $msg = $this->filterCommonOutgoingParameters($msg);
        $this->pushRaw("<presence to='".$msg['to']."' ".(@$msg['from']?("from='".$msg['from']."' "):"")."type='subscribed'/>\n");
        return $this;
    }

    // }}}
    // {{{ pushSubscriptionRequest()

    /**
     * Pushes the specified subscription request message to the list of messages to send
     *
     * @param   array              $msg the message
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushSubscriptionRequest($msg)
    {
        $msg = $this->filterCommonOutgoingParameters($msg);
        $this->pushRaw("<presence ".(@$msg['id']?"id='{$msg['id']}' ":"")."to='".$msg['to']."' ".(@$msg['from']?("from='".$msg['from']."' "):"")."type='subscribe'".(@$msg['msg']?"><status>{$msg['msg']}</status></presence>":"/>")."\n");
        return $this;
    }

    // }}}
    // {{{ pushUnsubscriptionConfirmation()

    /**
     * Pushes the specified unsubscription confirmation message to the list of messages to send
     *
     * @param   array              $msg the message
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushUnsubscriptionConfirmation($msg)
    {
        $msg = $this->filterCommonOutgoingParameters($msg);
        $this->pushRaw("<presence to='".$msg['to']."' ".(@$msg['from']?("from='".$msg['from']."' "):"")."type='unsubscribed'/>\n");
        return $this;
    }

    // }}}
    // {{{ pushUnsubscriptionRequest()

    /**
     * Pushes the specified unsubscription request message to the list of messages to send
     *
     * @param   array              $msg the message
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushUnsubscriptionRequest($msg)
    {
        $msg = $this->filterCommonOutgoingParameters($msg);
        $this->pushRaw("<presence to='".$msg['to']."' ".(@$msg['from']?("from='".$msg['from']."' "):"")."type='unsubscribe'/>\n");
        return $this;
    }

    // }}}
    // {{{ pushError()

    /**
     * Pushes the specified error message to the list of messages to send
     *
     * @param   array              $msg the message
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushError($msg)
    {
        $msg = $this->filterCommonOutgoingParameters($msg,array('code','message'));
        $this->pushRaw("<iq type='error' to='".$msg['to']."' id='{$msg['id']}'><error code='".$msg['code']."'>".$msg['message']."</error></iq>\n");
        return $this;
    }

    // }}}
    // {{{ pushPresence()

    /**
     * Pushes the specified presence message to the list of messages to send
     *
     * @param   array              $msg the message
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushPresence($msg)
    {
        $msg = $this->filterCommonOutgoingParameters($msg);
        $this->pushRaw(
            "<presence" . 
                  (@$msg['to'] ? (" to='".$msg['to']."'") : '')
                  . (@$msg['type'] ? " type='{$msg['type']}'" : '')
                  . ((@$msg['status'] || @$msg['show'] || @$msg['priority']) ? ">" : " />")
                  . ($msg['show'] ? ("    <show>".$msg['show']."</show>") : '')
                  . (@$msg['status'] ? ("    <status>".$msg['status']."</status>") : '')
                  . (@$msg['priority'] ? "    <priority>{$msg['priority']}</priority>" : '')
                  . ((@$msg['status'] || @$msg['show'] || @$msg['priority']) ? "</presence>\n" : '')
        );
        return $this;
     }

    // }}}
    // {{{ pushListContactRequest()

    /**
     * Pushes the specified contact list event to the list of messages to send
     *
     * @param   array              $msg the message
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushListContactRequest($msg)
    {
        $msg = $this->filterCommonOutgoingParameters($msg);
        $this->pushRaw("<iq type='get' id='{$msg['id']}'><query xmlns='jabber:iq:roster'></query></iq>\n");
        return $this;
    }

    // }}}
    // {{{ pushContactVCardRequest()

    /**
     * Pushes the specified contact VCard request event to the list of messages to send
     *
     * @param   array              $msg the message
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushContactVCardRequest($msg)
    {
        $msg = $this->filterCommonOutgoingParameters($msg,array('contact'));
        $this->pushRaw("<iq type='get' id='{$msg['id']}' to='{$msg['contact']}'><vCard xmlns='vcard-temp'/></iq>\n");
        return $this;
    }

    // }}}
    // {{{ pushAddContact()

    /**
     * Pushes the specified contact add event to the list of messages to send
     *
     * @param   array              $event the event
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushAddContact($event)
    {
        $event = $this->filterCommonOutgoingParameters($event);
        $this->pushRaw("<iq type='set' id='{$event['id']}'><query xmlns='jabber:iq:roster'><item jid='".$event['contact']."'/></query></iq>\n");
        return $this;
    }

    // }}}
    // {{{ pushRemoveContact()

    /**
     * Pushes the specified contact remove event to the list of messages to send
     *
     * @param   array              $event the event
     * @return  LightBot_IProtocolAdapter $this
     */
    public function pushRemoveContact($event)
    {
        $event = $this->filterCommonOutgoingParameters($event);
        $this->pushRaw("<iq type='set' id='{$event['id']}'><query xmlns='jabber:iq:roster'><item subscription='remove' jid='".$event['contact']."'/></query></iq>\n");
        return $this;
    }

    // }}}
    // {{{ pushRemoteAction()

    /**
     * Pushes the specified remote action to the list of remote action to execute
     *
     * @param string the remote uri of the action
     * @return LightBot_IProtocolAdapter $this
     */

    public function pushRemoteAction($uri,$params=null)
    {
        $errno  = null;
        $errstr = null;

        $urlInfo = parse_url($uri);

        switch($urlInfo['scheme']){
        case 'http':
            // timeout is 1 second
            $fp = @stream_socket_client('tcp://'.$urlInfo['host'].(@$urlInfo['port']?":{$urlInfo['port']}":':80'), $errno, $errstr, 1);
            if (!is_resource($fp) || feof($fp)) throw new Exception("Unable to connect to '$uri': Error #$errno $errstr");
            socket_set_blocking($fp,0);
            $userAgent = "LightBot";
            if($params!==null && is_array($params)){
                // POST mode
#                echo "-------------------------------------\n";
                $this->write($fp,"POST ".$urlInfo['path'].(@$urlInfo['query']?"?{$urlInfo['query']}":"").(@$urlInfo['fragment']?"?{$urlInfo['fragment']}":"")." HTTP/1.1\r\n");
#                echo "POST ".$urlInfo['path'].(@$urlInfo['query']?"?{$urlInfo['query']}":"").(@$urlInfo['fragment']?"?{$urlInfo['fragment']}":"")." HTTP/1.1\r\n";
                $this->write($fp,"Host: ".$urlInfo['host']."\r\n");
#                echo "Host: ".$urlInfo['host']."\r\n";
                $this->write($fp,"Accept: */*\r\n");
#                echo "Accept: */*\r\n";
                $this->write($fp,"Connection: close\r\n");
#                echo "Connection: close\r\n";
                $this->write($fp,"User-Agent: ".$userAgent."\r\n");
#                echo "User-Agent: ".$userAgent."\r\n";
                $this->write($fp,"Content-Type: application/x-www-form-urlencoded\r\n");
#                echo "Content-Type: application/x-www-form-urlencoded\r\n";
                $tp = '';
                foreach($params as $p=>$v){
                    if(is_string($v)){
                        $tp .= '&'.$p.'='.urlencode($v);
                    }else{
                        $tp .= '&'.$p.'='.urlencode(json_encode($v));
                    }
                }
                $this->write($fp,"Content-Length: ".strlen($tp)."\r\n");
#                echo "Content-Length: ".strlen($tp)."\r\n";
                $this->write($fp,"\r\n");
#                echo "\r\n";
                stream_set_write_buffer($fp, 0);
                $this->write($fp,$tp."\r\n");
#                echo $tp."\r\n";
#                echo "-------------------------------------\n";
                $this->getLightBot()->getLogger()->debug("HTTP POST --> $uri");
            }else{
                // GET mode
                $this->write($fp,"GET ".$urlInfo['path'].(@$urlInfo['query']?"?{$urlInfo['query']}":"").(@$urlInfo['fragment']?"?{$urlInfo['fragment']}":"")." HTTP/1.1\r\n");
                $this->write($fp,"Host: ".$urlInfo['host']."\r\n");
                $this->write($fp,"Accept: */*\r\n");
                $this->write($fp,"Connection: close\r\n");
                $this->write($fp,"User-Agent: ".$userAgent."\r\n");
                $this->write($fp,"\r\n");
                $this->getLightBot()->getLogger()->debug("HTTP GET --> $uri");
            }
            break;
        default:
            stream_socket_shutdown($fp, STREAM_SHUT_RDWR);
            throw new Exception("Protocol of uri '$uri' is not supported");
        }

        $this->remoteActions['handles'][] = $fp;
        $this->remoteActions['urls'][] = $uri;
    }

    // }}}
    // {{{ write()

    protected function write($fp,$raw)
    {
        while(($towrite = strlen($raw))>0){
            $written = fputs($fp,$raw,1024);
            $raw = substr($raw,$written);
        };
    }

    // }}}
}   

// }}}

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * c-hanging-comment-ender-p: nil
 * End:
 */
